﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using AxeFrog.Data.Expressions;
using AxeFrog.Logging;

namespace AxeFrog.Data
{
	/// <summary>
	/// Creates a new database connection or subscribes to a connection already created
	/// at a higher level, if it exists. This class should be used within a using() block
	/// as it is designed to manage its subscription to a connection via construction and
	/// subsequent disposal. Inside the using() block, a Transaction can be created which
	/// will automatically subscribe to the connection managed by the current ConnectionContext
	/// instance. Because of this behaviour, contained methods can be called which can create
	/// and dispose their own ConnectionContext and Transactions and the single connection and
	/// transaction will be used irrespective of the stack depth. Use the Specify(name) method
	/// to select a connection string other than the default ("ConnectionString").
	/// </summary>
	[Export, PartCreationPolicy(CreationPolicy.NonShared)]
	public class ConnectionContext : IDisposable
	{
		private readonly ConnectionHandle _handle;
		private string _name;
		private readonly Dictionary<string, string> _connectionStrings;
		private readonly MasterLogger _logger;
		private bool _initialised;
		private IConnectionProvider _connectionProvider;

		[ImportingConstructor]
		protected ConnectionContext(
			[Import(RequiredCreationPolicy = CreationPolicy.Shared)] ConnectionHandle handle,
			[ImportMany("ConnectionStrings")] Dictionary<string, string>[] connectionStrings,
			[ImportMany("ConnectionProviderName")] string[] connectionProviderNames,
			[ImportMany] IEnumerable<Lazy<IConnectionProvider, IConnectionProviderMetadata>> connectionProviders,
			MasterLogger logger)
		{
			var connectionProviderName = connectionProviderNames.Where(n => n != null).FirstOrDefault();
			foreach(Lazy<IConnectionProvider, IConnectionProviderMetadata> cp in connectionProviders)
				if(cp.Metadata.ProviderName == connectionProviderName)
				{
					_connectionProvider = cp.Value;
					break;
				}
			if(_connectionProvider == null)
			{
#if DEBUG
				if(Debugger.IsAttached)
					Debugger.Break();
#endif
				throw new Exception("No connection provider exists with the name \"" + connectionProviderName + "\"");
			}
			_connectionStrings = connectionStrings.Where(cs => cs != null).FirstOrDefault();
			_logger = logger;
			_handle = handle;
		}

		public ConnectionContext(Factory factory) : this(
			factory.Create<ConnectionHandle>(),
			factory.CreateAll<Dictionary<string, string>>("ConnectionStrings").ToArray(),
			factory.CreateAll<string>("ConnectionProviderName").ToArray(),
			factory.QueryAll<IConnectionProvider, IConnectionProviderMetadata>(),
			factory.Create<MasterLogger>())
		{
		}

		public ConnectionContext Specify(string name)
		{
			if(_name != null)
				throw new Exception("The name of the connection to be used can only be set once. To use a different connection, create a new ConnectionContext object.");
			_name = name;
			return this;
		}

		private void CheckInitialize()
		{
			if(_initialised) return;
			if(_name == null)
				_name = "ConnectionString";
			string connectionString;
			if(!_connectionStrings.TryGetValue(_name, out connectionString))
				throw new Exception("The name \"" + _name + "\" does not exist in the ConnectionStrings collection");
			var count = _handle.GetConnectionUsageCount(_name);
			if(count == 0)
			{
				_handle.SaveConnection(ConnectionProvider.RequestConnection(connectionString), _name);
				//_logger.WriteLine("Connections", "--> OPEN");
			}
			_handle.SetConnectionUsageCount(_name, count + 1);
			_initialised = true;
		}

		public virtual SqlExpressionVisitor CreateExpressionVisitor(IDbCommand cmd)
		{
			return new SqlExpressionVisitor(cmd);
		}

		public IDbConnection Connection
		{
			get
			{
				CheckInitialize();
				return _handle.RetrieveConnection(_name);
			}
		}

		public IDbTransaction Transaction
		{
			get
			{
				CheckInitialize();
				return _handle.RetrieveTransaction(_name);
			}
		}

		public IConnectionProvider ConnectionProvider
		{
			get { return _connectionProvider; }
		}

		public TransactionContext BeginTransaction()
		{
			CheckInitialize();
			return new TransactionContext(_handle, _name);
		}

		public void Dispose()
		{
			if(!_initialised) return;
			var count = _handle.GetConnectionUsageCount(_name);
			_handle.SetConnectionUsageCount(_name, --count);
			if(count == 0)
			{
				IDbConnection connection = _handle.RetrieveConnection(_name);
				connection.Close();
				connection.Dispose();
				_handle.ReleaseConnection(_name);
				//_logger.WriteLine("Connections", "<-- CLOSE");
			}
		}

		public class TransactionContext : IDisposable
		{
			private readonly ConnectionHandle _handle;
			private readonly string _name;
			private IDbTransaction _trans;
			private bool _committed;
			internal TransactionContext(ConnectionHandle handle, string name)
			{
				_handle = handle;
				_name = name;
				var count = _handle.GetTransactionUsageCount(_name);
				_handle.SetTransactionUsageCount(_name, ++count);
				if(count == 1)
				{
					var conn = _handle.RetrieveConnection(_name);
					_trans = conn.BeginTransaction();
					_handle.SaveTransaction(_trans, _name);
				}
			}

			public void Commit()
			{
				_committed = true;
			}

			public void Dispose()
			{
				if(!_committed)
					_handle.SetTransactionCancelled(_name, true);

				var count = _handle.GetTransactionUsageCount(_name);
				_handle.SetTransactionUsageCount(_name, --count);
				if(count == 0)
				{
					if(_handle.IsTransactionCancelled(_name))
						_trans.Rollback();
					else
						_trans.Commit();
					_trans.Dispose();
					_trans = null;
					_handle.ReleaseTransaction(_name);
					_handle.SetTransactionCancelled(_name, false);
				}
			}
		}
	}
}